package sk.uniba.fmph.asdf;

import java.awt.Image;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Stack;
import java.util.prefs.Preferences;

import javax.imageio.ImageIO;

import sk.uniba.fmph.asdf.db.Account;
import sk.uniba.fmph.asdf.db.SQLQuery;
import sk.uniba.fmph.asdf.db.SQLQueryHandler;
import sk.uniba.fmph.asdf.exceptions.DatabaseException;
import sk.uniba.fmph.asdf.exceptions.OfflineException;
import sk.uniba.fmph.asdf.exceptions.PermissionDeniedException;
import sk.uniba.fmph.asdf.tempt.PrimaryTestFieldDef;
import sk.uniba.fmph.asdf.tempt.TestFieldDef;
import sk.uniba.fmph.asdf.tempt.TestTemplate;
import sk.uniba.fmph.asdf.test.Comment;
import sk.uniba.fmph.asdf.test.PrimaryTestField;
import sk.uniba.fmph.asdf.test.Stamp;
import sk.uniba.fmph.asdf.test.StampType;
import sk.uniba.fmph.asdf.test.Test;
import sk.uniba.fmph.asdf.test.TestType;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;

/**
 *
 * @author Jakub Gaľ
 * @author Ivor Uhliarik
 */
public class Session {

	/** Username pre SSH pripojenie k serveru */
	// TODO: iny account pre pripojenie
	static final String SSH_USERNAME = "testy";

	/** Hostname servera pre SSH pripojenie */
	static final String SSH_HOST = "kempelen.ii.fmph.uniba.sk";

	/** Heslo pre SSH pripojenie k serveru */
	static final String SSH_PASSWORD = "Password1";

	/** Cesta k adresaru s obrazkami testov na serveri */
	static final String SSH_IMAGE_DIR = "images";

	/** Pripona obrazkov testov na serveri */
	static final String SSH_IMAGE_EXT = ".png";

	/** Aktivny ucet pouzivatela. */
	private static Account account;

	/** Online stav */
	private static boolean online;

	/** Zoznam testov, ktore ma pouzivatel k dispozicii. */
	private static HashSet<Test> tests = new HashSet<>();

	/**
	 * Zoznam sablon, ktore sa vytvorili pocas tejto relacie. Sluzi ako pomocny
	 * zoznam pre vytvaranie testu. Sablona priradena testu by sa bezne mala
	 * ziskavat z objektu triedy Test.
	 */
	private static ArrayList<TestTemplate> createdTemplates = new ArrayList<>();

	/** Stack stavovych sprav programu */
	private static final Stack<String> statusMessages = new Stack<>();

	/** Relacia pre SFTP pripojenie */
	private static com.jcraft.jsch.Session sftpSession = null;

	/** Kanal SFTP pripojenia */
	private static ChannelSftp sftpChannel = null;

    private static Preferences preferences;

    static {
        preferences = Preferences.userRoot();
    }

    public static Preferences getPreferences() {
        return preferences;
    }

	/**
	 * Vrati aktivny ucet pouzivatela.
	 *
	 * @return Aktivny ucet pouzivatela.
	 */
	public static Account getAccount() {
		return account;
	}

	/**
	 * Nastavi aktivny ucet pouzivatela.
	 *
	 * @param newAccount
	 *            Ucet pouzivatela.
	 */
	public static void setAccount(Account newAccount) {
		account = newAccount;
		File dir = new File(getUserDir() + Exporter.DIR_TEMPLATES);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		dir = new File(getUserDir() + Exporter.DIR_TESTS);
		if (!dir.exists()) {
			dir.mkdirs();
		}
        dir = new File(getUserDir() + Exporter.DIR_EXPORT);
		if (!dir.exists()) {
			dir.mkdirs();
		}
	}

	/**
	 * Vrati zoznam testov, ktore ma aktivny pouzivatel k dispozicii.
	 *
	 * @return Zoznam dostupnych testov.
	 */
	public static HashSet<Test> getTests() {
		return tests;
	}

	/**
	 * Nastavi zoznam testov, ktore ma aktivny pouzivatel k dispozicii.
	 *
	 * @param newTests
	 *            Zoznam testov, ktore ma aktivny pouzivatel k dispozicii.
	 */
	public static void setTests(HashSet<Test> newTests) {
		tests = newTests;
	}

	/**
	 * Prida do relacie vytvorenu sablonu.
	 *
	 * @param template
	 *            Sablona testu.
	 */
	public static void addTemplate(TestTemplate template) {
		createdTemplates.add(template);
	}

	/**
	 * Metoda nacita serializovany test zo suboru.
	 *
	 * @param filename
	 *            meno suboru, v ktorom je test serializovany
	 * @return nacitany test
	 * @throws IOException
	 *             ak zlyhala input/output operacia
	 * @throws ClassNotFoundException
	 *             nespravna definicia triedy
	 */
	public static Test importTest(String filename) throws IOException,
			ClassNotFoundException {
		FileInputStream fileInputStream =
				new FileInputStream(filename + ".ser");
		ObjectInputStream objectInputStream =
				new ObjectInputStream(fileInputStream);
		Test test = (Test) objectInputStream.readObject();
		objectInputStream.close();
		fileInputStream.close();
		return test;
	}

	/**
	 * Vytvori novy test dbajuc na typ testu. Ak ide o typ, ktory je
	 * hodnotitelny kazdym, vytvori sa specificky test pre kazdeho hodnotitela.
	 *
	 * @param type
	 *            Typ testu.
	 * @param studentName
	 *            Meno studenta, ktory test vyplnil.
	 * @param evaluatorAccount
	 *            Ucet hodnotitela testu.
	 * @param template
	 *            Sablona, ktora sa k testu priradi.
	 * @param image
	 *            Obrazok naskenovaneho testu.
	 * @throws PermissionDeniedException
	 *             Ak pouzivatel nema pravo vytvorit test.
	 */
	public static void createTest(final TestType type,
			final String studentName, final Account evaluatorAccount,
			final TestTemplate template, final Image image) throws PermissionDeniedException {
		if (!template.getAuthorAccount().equals(account)) {
			throw new PermissionDeniedException(
					"Test môže vytvárať jedine autor šablóny.");
		}
		if (type == TestType.REGULAR) {
			createSpecificTest(type, studentName, evaluatorAccount, template,
					image);
		} else {
			for (Account evaluator : template.getEvaluatorAccounts()) {
				createSpecificTest(type, studentName, evaluator, template,
						image);
			}
		}
	}

	/**
	 * Vytvori novy specificky test na zaklade obrazka testu pre daneho
	 * hodnotitela a dodatocne ho ulozi do lokalneho suboru.
	 *
	 * @param type
	 *            Typ testu.
	 * @param studentName
	 *            Meno studenta, ktory test vyplnil.
	 * @param evaluator
	 *            Ucet hodnotitela testu.
	 * @param template
	 *            Sablona, ktora sa k testu priradi.
	 * @param image
	 *            Obrazok naskenovaneho testu.
	 */
	public static void createSpecificTest(final TestType type,
			final String studentName, final Account evaluatorAccount,
			final TestTemplate template, final Image image) {
		Test test =
				new Test(type, studentName, evaluatorAccount, template, image);
		tests.add(test);

		// Testu vytvorime oblasti podla definicii
		for (PrimaryTestFieldDef fieldDef : template.getFieldDefs()) {
			PrimaryTestField field = new PrimaryTestField(fieldDef);
			test.addField(field);
		}

		try {
			Exporter.serializeEntity(test, test.toString() + "-"
					+ test.getEvaluatorAccount().getEmail(), Exporter.DIR_TESTS);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Stiahne neaktualne udaje z databazy a aktualizuje udaje v databaze nasimi
	 * zmenami.
	 *
	 * @throws DatabaseException
	 *             Ak nastala chyba pri ziskavani udajov.
	 * @throws JSchException
	 *             Ak sa nepodarilo pripojit na server.
	 * @throws SftpException
	 *             Ak sa nepodaril prenos suboru zo servera.
	 * @throws IOException
	 *             Ak nastala chyba pocas citania obrazka alebo serializacie.
	 */
	public static void synchronize() throws DatabaseException, JSchException,
			SftpException, IOException, OfflineException {
		openSftpConnection();

		uploadDatabaseData();
		downloadDatabaseData();

		closeSftpConnection();

		saveServerData();
	}

    /**
     * Metoda synchronizuje internu pamat a lokalne subory.
     * @throws IOException ak zlyhala input/output operacia
     * @throws ClassNotFoundException ak nastala chyba pri serializacii
     */
	public static void synchronizeMemoryAndFiles() throws IOException,
			ClassNotFoundException {
		saveServerData();

		ArrayList<TestTemplate> localTemplates = loadLocalTemplates();
        for (TestTemplate template : localTemplates) {
            if (!createdTemplates.contains(template)) {
                createdTemplates.add(template);
            }
        }

        ArrayList<Test> localTests = loadLocalTests();
        for (Test test : localTests) {
        	for (TestTemplate template : localTemplates) {
        		if (test.getTemplateTimestamp() == template.getTimestamp()) {
        			test.assignToTemplate(template);
        			break;
        		}
        	}
        }
        for (Test test : localTests) {
        	boolean found = false;
        	for (Test oldTest : tests) {
        		if (test.equals(oldTest)) {
        			found = true;
        			break;
        		}
        	}
        	if (!found) {
        		tests.add(test);
        	}
        }
	}


    /**
     * Nacitanie lokalne ulozenych sablon.
     * @return zoznam sablon
     * @throws IOException ak zlyhala input/output operacia
     * @throws ClassNotFoundException ak nastala chyba pri serializacii
     */
	private static ArrayList<TestTemplate> loadLocalTemplates()
			throws IOException, ClassNotFoundException {
		File dir = new File(getUserDir() + Exporter.DIR_TEMPLATES);

		File[] files = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				return filename.endsWith(".ser");
			}
		});

		ArrayList<TestTemplate> templates = new ArrayList<>();
		for (File file : files) {
			templates.add(TestTemplate.read(file));
		}
		return templates;
	}

    /**
     * Nacitanie lokalne ulozenych testov.
     * @return zoznam testov
     * @throws IOException ak zlyhala input/output operacia
     * @throws ClassNotFoundException ak nastala chyba pri serializacii
     */
	private static ArrayList<Test> loadLocalTests() throws IOException,
			ClassNotFoundException {
		File dir = new File(getUserDir() + Exporter.DIR_TESTS);

		File[] files = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String filename) {
				return filename.endsWith(".ser");
			}
		});

		ArrayList<Test> tests = new ArrayList<>();
		for (File file : files) {
			tests.add(Test.read(file));
		}
		return tests;
	}

    /**
     * Ulozenie serverovych dat do lokalnych suborov.
     * @throws IOException ak zlyhala input/output operacia
     */
	private static void saveServerData() throws IOException {
		for (Test test : tests) {
			Exporter.serializeEntity(test.getTemplate(), test.getTemplate()
					.toString(), Exporter.DIR_TEMPLATES, true);
			Exporter.serializeEntity(test, test.toString() + "-"
					+ test.getEvaluatorAccount().getEmail(), Exporter.DIR_TESTS);
			Exporter.saveTestImage(test);
		}
	}

	/**
	 * Stiahne nove, zmenene testy, sablony a ich vsetky udaje z databazy.
	 *
	 * @throws DatabaseException
	 *             Ak nastala chyba pri ziskavani udajov.
	 * @throws JSchException
	 *             Ak sa nepodarilo pripojit sa na server.
	 * @throws SftpException
	 *             Ak sa nepodaril prenos suboru zo servera.
	 * @throws IOException
	 *             Ak nastala chyba pocas citania obrazka.
	 */
	@SuppressWarnings("unchecked")
	private static void downloadDatabaseData() throws DatabaseException,
			JSchException, SftpException, IOException, OfflineException {
		SQLQueryHandler queryHandler = new SQLQueryHandler();
		queryHandler.openConnection();

		// Zistime cas poslednej synchronizacie podla najdavnejsej synchronizacie testu
		Timestamp lastSynchronizationDate = null;
		for (Test test : tests) {
			if (lastSynchronizationDate == null
					|| test.getLastSynchronizationDate().compareTo(
							lastSynchronizationDate) < 0) {
				lastSynchronizationDate = test.getLastSynchronizationDate();
			}
		}
		// Ak nebol synchronizovany ziadny test, berieme vsetky testy (davny datum)
		if (lastSynchronizationDate == null) {
			lastSynchronizationDate = new Timestamp(1000);
		}

		// Ziskame testy
		ArrayList<Test> newTests =
				(ArrayList<Test>) queryHandler.getResultSet(
						SQLQuery.GET_MODIFIED_TESTS, account.getId(),
						lastSynchronizationDate.toString());

		HashSet<TestTemplate> existingTemplates = new HashSet<>(getAllTemplates());
		for (Test test : newTests) {
			SQLQueryHandler helpQueryHandler = new SQLQueryHandler();
			helpQueryHandler.openConnection();

			// Ku kazdemu testu ziskame sablonu a priradime mu ju
			TestTemplate template =
					(TestTemplate) helpQueryHandler.getResultSet(
							SQLQuery.GET_TEMPLATE, test.getId());
			// Ak uz mame ekvivalentnu sablonu, pouzijeme radsej tu
			TestTemplate equalExistingTemplate = null;
			for (TestTemplate existingTemplate : existingTemplates) {
				if (existingTemplate.equals(template)) {
					equalExistingTemplate = existingTemplate;
					break;
				}
			}
			if (equalExistingTemplate != null) {
				test.assignToTemplate(equalExistingTemplate);
			} else {
				// Ak nemame, pouzijeme stiahnutu
				test.assignToTemplate(template);
				existingTemplates.add(template);
			}

			// Pre kazdy test zistime, ake ma rozsirenia oblasti
			ArrayList<TestFieldDef> fieldExtensions =
					(ArrayList<TestFieldDef>) helpQueryHandler.getResultSet(
							SQLQuery.GET_FIELD_EXTENSIONS, test.getId());

			// Testu vytvorime oblasti podla definicii
			for (PrimaryTestFieldDef fieldDef : template.getFieldDefs()) {
				PrimaryTestField field = new PrimaryTestField(fieldDef);
				test.addField(field);
				// Definicii pridame rozsirenie, ak ho ma mat
				for (TestFieldDef fieldExtension : fieldExtensions) {
					if (fieldExtension.getParentId() == fieldDef.getId()) {
						field.addFieldExtension(fieldExtension);
					}
				}
			}

			// Ku kazdemu testu ziskame peciatky a pridame mu ich
			ArrayList<Stamp> stamps =
					(ArrayList<Stamp>) helpQueryHandler.getResultSet(
							SQLQuery.GET_STAMPS, account.getId(), test.getId());
			for (Stamp stamp : stamps) {
				if (stamp.getStampType() == StampType.COMMENT) {
					test.addComment((Comment) stamp);
				} else {
					test.addStamp(stamp);
				}
			}

			// K testu stiahneme obrazok a priradime mu ho
			test.setImage(downloadImage("image" + test.getId() + SSH_IMAGE_EXT));

			helpQueryHandler.closeConnection();

			// Nastavime cas synchronizacie
			test.setLastSynchronizationDateToNow();
		}
		queryHandler.closeConnection();

		// Vymazeme vsetky lokalne testy, ktore maju byt nahradene novymi
//		for (int i = 0; i < tests.size(); i++) {
//			for (Test newTest : newTests) {
//				if (tests.get(i).getId() == newTest.getId()) {
//					tests.remove(i);
//				}
//			}
//		}
		HashSet<Test> testsToRemove = new HashSet<Test>();
		for (Test test : tests) {
			for (Test newTest : newTests) {
				if (test.getId() == newTest.getId()) {
					testsToRemove.add(test);
				}
			}
		}
		tests.removeAll(testsToRemove);

		// Do lokalnych testov pridame nove testy
		tests.addAll(newTests);
	}

	/**
	 * Stiahne a vrati obrazok testu zo servera podla mena suboru.
	 *
	 * @param fileName
	 *            meno suboru obrazka, ktory sa ma stiahnut.
	 * @return Obrazok pre zobrazenie v GUI.
	 * @throws JSchException
	 *             Ak sa nepodarilo pripojit sa na server.
	 * @throws SftpException
	 *             Ak sa nepodaril prenos suboru zo servera.
	 * @throws IOException
	 *             Ak nastala chyba pocas citania obrazka.
	 */
	private static Image downloadImage(final String fileName)
			throws JSchException, SftpException, IOException {
		InputStream in = sftpChannel.get(SSH_IMAGE_DIR + "/" + fileName);

		Image image = ImageIO.read(in);

		in.close();
		return image;
	}

	/**
	 * Odosle lokalne zmenene testy, sablony a ich udaje databaze.
	 *
	 * @throws DatabaseException
	 *             Ak nastala chyba pri ziskavani udajov.
	 * @throws JSchException
	 *             Ak sa nepodarilo pripojit sa na server.
	 * @throws SftpException
	 *             Ak sa nepodaril prenos suboru zo servera.
	 * @throws IOException
	 *             Ak nastala chyba pocas citania obrazka.
	 */
	private static void uploadDatabaseData() throws DatabaseException,
			JSchException, SftpException, IOException, OfflineException {
		SQLQueryHandler queryHandler = new SQLQueryHandler();
		queryHandler.openConnection();

		// Posleme kazdy test, ktory sa zmenil
		for (Test test : tests) {
			// Ak test nebol zmeneny od poslednej synchronizacie, preskocime ho
			if (test.getId() != -1
					&& test.getLastModificationDate().compareTo(
							test.getLastSynchronizationDate()) <= 0) {
				continue;
			}

			// Posleme udaje o sablone, ak este nema pridelene ID
			if (test.getTemplate().getId() == -1) {
				TestTemplate template = test.getTemplate();
				int templateId =
						queryHandler.executeUpdateReturningId(
								SQLQuery.ADD_TEMPLATE, template
										.getAuthorAccount().getId(), template
										.getName());
				// Sablone nastavime ID
				template.setId(templateId);

				// Posleme definicie oblasti
				for (PrimaryTestFieldDef fieldDef : template.getFieldDefs()) {
					float x = fieldDef.getTopLeft().x;
					float y = fieldDef.getTopLeft().y;
					float width = fieldDef.getBottomRight().x - x;
					float height = fieldDef.getBottomRight().y - y;
					int fieldDefId =
							queryHandler.executeUpdateReturningId(
									SQLQuery.ADD_FIELD, template.getId(), x, y,
									width, height, fieldDef.getMaxPoints());
					// Definicii oblasti nastavime ID
					fieldDef.setId(fieldDefId);
				}
			}

			if (test.getId() == -1) {
				// Posielame udaje o novom teste
				int testId =
						queryHandler.executeUpdateReturningId(
								SQLQuery.ADD_TEST, test.getEvaluatorAccount()
										.getId(), test.getTemplate().getId(),
								test.getType().ordinal(), test.getStudentName()
										.toString());
				// Testu nastavime ID
				test.setId(testId);
			} else {
				// Aktualizujeme zmeny testu
				queryHandler.executeUpdate(SQLQuery.UPDATE_TEST, test.getId(),
						test.getStudentName());
			}

			// Uploadneme obrazok testu
			uploadImage(test);

			// Testu nastavime terajsi cas synchronizacie
			test.setLastSynchronizationDateToNow();

			for (PrimaryTestField field : test.getFields()) {
				// Posleme peciatky
				for (Stamp stamp : field.getStamps()) {
					String comment = "";
					if (stamp.getStampType() == StampType.COMMENT) {
						comment = ((Comment) stamp).getValue();
					}
					queryHandler.executeUpdate(SQLQuery.ADD_STAMP, test
							.getEvaluatorAccount().getId(), test.getId(), field
							.getTestFieldDef().getId(), stamp.getStampType()
							.ordinal(), stamp.getPosition().x, stamp
							.getPosition().y, comment);
				}

				// Posleme rozsirenia oblasti
				TestFieldDef fieldExtension = field.getFieldExtension();
				if (fieldExtension != null) {
					float x = fieldExtension.getTopLeft().x;
					float y = fieldExtension.getTopLeft().y;
					float width = fieldExtension.getBottomRight().x - x;
					float height = fieldExtension.getBottomRight().y - y;
					int extId =
							queryHandler.executeUpdateReturningId(
									SQLQuery.ADD_FIELD_EXTENSION, test.getId(),
									field.getTestFieldDef().getId(), x, y,
									width, height);
					fieldExtension.setId(extId);
				}
			}
		}
		queryHandler.closeConnection();
	}

	/**
	 * Posle obrazok testu na server.
	 *
	 * @param test
	 *            Test, ktoremu patri obrazok.
	 * @throws JSchException
	 *             Ak sa nepodarilo pripojit sa na server.
	 * @throws SftpException
	 *             Ak sa nepodaril prenos suboru zo servera.
	 * @throws IOException
	 *             Ak nastala chyba pocas citania obrazka.
	 */
	private static void uploadImage(final Test test) throws JSchException,
			SftpException, IOException {
		makeDirs(sftpChannel);

		OutputStream out =
				sftpChannel.put(SSH_IMAGE_DIR + "/" + "image" + test.getId()
						+ SSH_IMAGE_EXT);
		ImageIO.write((RenderedImage) test.getImage(), "png", out);
		out.close();
	}

	private static void makeDirs(final ChannelSftp channel)
			throws SftpException {
		SftpATTRS attrs;
		try {
			attrs = channel.stat(SSH_IMAGE_DIR);
			if (attrs == null || !attrs.isDir()) {
				channel.mkdir(SSH_IMAGE_DIR);
			}
		} catch (SftpException e) {
			channel.mkdir(SSH_IMAGE_DIR);
		}
	}

	/**
	 * Nadviaze SFTP spojenie so serverom.
	 *
	 * @throws JSchException
	 *             Ak sa nepodarilo nadviazat spojenie.
	 */
	private static void openSftpConnection() throws JSchException {
		JSch jsch = new JSch();
		sftpSession = jsch.getSession(SSH_USERNAME, SSH_HOST);
		sftpSession.setConfig("StrictHostKeyChecking", "no");
		sftpSession.setPassword(SSH_PASSWORD);
		sftpSession.connect();

		Channel channel = sftpSession.openChannel("sftp");
		channel.connect();
		sftpChannel = (ChannelSftp) channel;
	}

	/**
	 * Zavrie SFTP spojenie so serverom.
	 */
	private static void closeSftpConnection() {
		if (sftpChannel != null) {
			sftpChannel.exit();
			sftpChannel = null;
		}
		if (sftpSession != null) {
			sftpSession.disconnect();
			sftpSession = null;
		}
	}

	public static void addStatusMessage(String message) {
		statusMessages.push(message);
	}

	public static String[] getStatusMessages() {
		return statusMessages.toArray(new String[statusMessages.size()]);
	}

	public static boolean isLoggedIn() {
		return (account != null);
	}

    /**
     * Metoda vracia online status
     * @return true ak je uzivatel online, v opacnom pripade false
     */
	public static boolean isOnline() {
		return online;
	}

    /**
     * Metoda nastavi online status
     * @param status novy status
     */
	public static void setOnline(boolean status) {
		online = status;
	}

    /**
     * Metoda vrati nazov zlozky uzivatela.
     * @return nazov zlozky uzivatela
     */
	public static String getUserDir() {
		return account.getEmail() + "/";
	}

	/**
	 * Vrati mnozinu vsetkych sablon testov zjednotenu so sablonami vytvorenymi
	 * pocas behu programu (createdTemplates).
	 *
	 * @return Mnozina vsetkych sablon.
	 */
	public static HashSet<TestTemplate> getAllTemplates() {
		HashSet<TestTemplate> allTemplates = new HashSet<>(createdTemplates);
		for (Test test : tests) {
			allTemplates.add(test.getTemplate());
		}
		return allTemplates;
	}
}
